Pinvon's Blog

所见, 所闻, 所思, 所想

Make

概述

编译(compile): 使用编译器, 将源代码转化为目标文件(.o).

链接(link): 把多个 .o 文件或 lib 库文件转化为可执行文件(.exe 或 .dll).

构建(build): 指用源代码生成可执行文件的整个过程, 如先编译哪个文件, 再编译哪个文件, 先链接哪个文件, 再链接哪个文件, 都属于构建的范畴.

学习 Make 的网址: GNU Make

Make

Make 根据指定的 Shell 命令进行构建. 我们应该告诉 Make, 有哪些文件要参与构建, 这些文件的依赖关系是怎样的, 当有文件变动时, 应如何重新构建它.

Makefile

Make 使用的构建规则, 都写在 Makefile 文件里面.

Makefile 文件的规则

一个 Makefile 文件内部包含了许多规则, 每条规则的形式如下:

<target> : <prerequisites>
[tab] <commands>

其中, <commands> 前面的 [tab] 表示一个 tab 键, 这是规定; <target> 是必需的; <prerequisites> 和 <commands> 都是可选的, 但两者必须至少有一个.

target

一个 target 构成一条规则. 一般来说, target 就是文件名, 如果有多个文件名, 使用空格分隔. 如:

main: main.o
    g++ main.o -o main
main.o: main.cpp
    g++ -c main.cpp -o main.o

这样就会生成一个可执行文件 main.

phony target

target 除了可以是某些文件名之外, 还可以是某个操作的名字, 称为 phony target. 如:

clean:
    rm *.o

当我们执行 make clean 时, 就相当于执行 rm *.o, 把所有 .o 文件删除了.

但是, 上面的写法有一些小问题. 如果当前目录下正好有一个叫做 clean 的文件, 则这条命令不会执行, 因为 Make 发现 clean 文件已经存在, 就不再重新构建了.

解决办法是将 clean 声明为 phony target. 如下:

.PHONY: clean
clean:
    rm *.o

这样, make 就不会去检查是否存在一个 clean 文件, 直接执行命令.

prerequisites

prerequisites 通常是一组文件名, 文件名之间用空格分隔. 如:

main: main.o scene.o layer.o
    g++ main.o scene.o layer.o -o main

重新构建目标 main 的情况: main.o scene.o layer.o 三个中有一个不存在, 或者有更新过(对比文件的最后修改时间和目标的时间). 如果 main.o 不存在, 则需要另外写一条规则来生成 main.o:

main.o: main.cpp
    g++ -c main.cpp -o main.o

commands

commands 就是一条或多条 Shell 命令, 每个命令前都必须有一个 tab 键. 注意, 不同行的 Shell 命令会在不同的 Shell 中执行, 这些 Shell 之间没有继承关系. 如:

var-lost:
    export foo=bar
    echo "foo=[$$foo]"

这样写是取不到 foo 的值的. 如果要想取到 foo 的值, 有以下几种办法:

  • 写在同一行, 使用分号分隔
    var-kept:
        export foo=bar; echo "foo=[$$foo]"
    
  • 写在不同行, 换行符前加反斜杠转义
    var-kept:
        export foo=bar; \
        echo "foo=[$$foo]"
    
  • 使用 .ONESHELL 命令
    .ONESHELL:
    var-kept:
        export foo=bar;
        echo "foo=[$$foo]"
    

Makefile 语法

注释 # 号

echo

make 会打印每条命令, 然后再执行. 如:

test:
    # this is a test!

执行 make test 后, 会显示 this is a test!.

如果在命令的前面加上 @ 符号, 就可以关闭输出. 如:

test:
    @# this is a test!

在构建的过程中, 我们常常需要了解当前正在执行哪条命令, 所以一般只在注释和 echo 命令前加上 @ 符号.

通配符

如果当前目录下有 1.c, 2.c, test.c, 1.h

print:
    @echo *.c       # 1.c, 2.c, test.c
    @echo ?.c       # 1.c, 2.c
    @echo [12].*    # 1.c, 1.h, 2.c

模式匹配

Make 命令允许对文件名进行类似正则运算的匹配, 主要是使用匹配符 %.

如果当前目录下有 f1.c, f2.c 两个源码文件, 需要将它们编译为对应的对象文件:

%.o: %.c

相当于:

f1.o: f1.c
f2.o: f2.c

使用 % 可以将大量同类型的文件, 只用一条规则就完成构建.

变量和赋值符

自定义变量

Makefile 可以使用等号自定义变量, 调用变量时, 需要将变量放在 $() 中间.

txt = hello world
test:
    @echo $(txt)

Shell 变量

调用 Shell 变量, 如 $HOME, 需要在 $HOME 前面再加 $ 符号进行转义. 如:

test:
    @echo $$HOME
= 操作符

= 无限递归, 可以使用后面的变量来定义前面的变量. 如:

foo = $(bar)
bar = $(ugh)
ugh = Huh?
all:
    echo $(foo)

执行 make all 后, 将会输出 Huh?.

:= 操作符
y := $(x) bar
x := foo

此时, 输出的 y 就是 bar, x 是 foo, 可以看出, y 的输出不会使用到后面才出现的变量 x.

?= 操作符
FOO ?= bar

如果 FOO 之前没有定义过, 则 FOO 的值就是 bar, 如果之前已经定义过, 则这条语句什么也不做.

+= 操作符
objects = main.o foo.o bar.o utils.o
objects += another.o

此时, objects 的值是 main.o foo.o bar.o utils.o another.o

内置变量

如 $(CC) 指向当前使用的编译器, $(MAKE) 指向当前使用的 Make 工具.

自动变量

  • $@: 表示规则中的目标文件集, 如果有多个目标, 则 $@ 就是这些目标的集合.
main.o: main.cpp
    g++ -c main.cpp -o $@  # $@ 表示 main.o

再如:

a.txt b.txt:
    touch $@

相当于:

a.txt:
    touch a.txt
b.txt:
    touch b.txt
  • $<: 表示 prerequisites 中的第一个. 如:
a.txt: b.txt c.txt
    cp $< $@

相当于:

a.txt: b.txt c.txt
    cp b.txt a.txt
  • $?: 表示 prerequisites 中比 target 更新的集合.
  • $^: 表示所有 prerequisites.
  • $*: 匹配 % 及 % 之前的部分. 如: dir/a.foo.b, 使用的模式为 dir/a.%.b, 则 $* 表示 dir/a.foo
  • $(@D): 指向 $@ 的目录名.
  • $(@F): 指向 $@ 的文件名.
  • $(<D): 指向 $< 的目录名.
  • $(<F): 指向 $< 的文件名.

判断和循环

判断和循环的语法与 Bash 一样.

判断编译器是否为 gcc, 然后指定不同的库文件:

ifeq ($(CC), gcc)
    libs=$(libs_for_gcc)
else
    libs=$(normal_libs)
endif

循环:

LIST = one two three
all:
    for i in $(LIST); do \
        echo $$i; \
    done

# 相当于

all:
    for i in one two three; do \
        echo $i; \
    done

函数

函数格式为: $(function arguments) 或 ${function arguments}.

  • shell 函数. 其参数就是 OS shell 的命令.
srcfiles := $(shell echo src/{00..99}.txt)
  • wildcard 函数: 该函数的功能是扩展通配符. 如下面的例子中, 将所有 src 目录下的 txt 文件存入 srcfiles 变量.
  • subst 函数: 文本替换.
$(subst arg1, arg2, text)

使用 arg2 来替换 text 中的 arg1.

  • patsubst 函数: 模式匹配的替换.
$(patsubst pattern, replacement, text)

如:

$(patsubst %.c, %.o, x.c.c bar.c)  # x.c.o bar.o

例子

编译 C 语言项目:

edit : main.o kbd.o command.o display.o 
    cc -o edit main.o kbd.o command.o display.o

main.o : main.c defs.h
    cc -c main.c
kbd.o : kbd.c defs.h command.h
    cc -c kbd.c
command.o : command.c defs.h command.h
    cc -c command.c
display.o : display.c defs.h
    cc -c display.c

clean :
     rm edit main.o kbd.o command.o display.o

.PHONY: edit clean

Comments

使用 Disqus 评论
comments powered by Disqus